INTRODUCCION¶

Este análisis tiene como objetivo examinar la relación entre ciertas condiciones médicas, como la presión arterial y los niveles de glucosa en sangre, y la diabetes. También se busca construir un modelo que pueda predecir si una persona tiene diabetes basándose en esta información, utilizando diferentes métodos y comparándolos entre sí. Es importante tener en cuenta que el conjunto de datos utilizado en este estudio se compone exclusivamente de mujeres mayores de 21 años, por lo que los resultados pueden variar significativamente en personas que no cumplan estas características.

Exploracion y Limpieza de los datos¶

In [1]:
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import  StandardScaler
from sklearn.metrics import classification_report
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier

import warnings
warnings.filterwarnings('ignore')
In [2]:
df = pd.read_csv('diabetes.csv')
df.head()
Out[2]:
Pregnancies Glucose BloodPressure SkinThickness Insulin BMI DiabetesPedigreeFunction Age Outcome
0 6 148 72 35 0 33.6 0.627 50 1
1 1 85 66 29 0 26.6 0.351 31 0
2 8 183 64 0 0 23.3 0.672 32 1
3 1 89 66 23 94 28.1 0.167 21 0
4 0 137 40 35 168 43.1 2.288 33 1

Primero, vamos a analizar la distribución de los datos.

In [3]:
df.describe()
Out[3]:
Pregnancies Glucose BloodPressure SkinThickness Insulin BMI DiabetesPedigreeFunction Age Outcome
count 768.000000 768.000000 768.000000 768.000000 768.000000 768.000000 768.000000 768.000000 768.000000
mean 3.845052 120.894531 69.105469 20.536458 79.799479 31.992578 0.471876 33.240885 0.348958
std 3.369578 31.972618 19.355807 15.952218 115.244002 7.884160 0.331329 11.760232 0.476951
min 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.078000 21.000000 0.000000
25% 1.000000 99.000000 62.000000 0.000000 0.000000 27.300000 0.243750 24.000000 0.000000
50% 3.000000 117.000000 72.000000 23.000000 30.500000 32.000000 0.372500 29.000000 0.000000
75% 6.000000 140.250000 80.000000 32.000000 127.250000 36.600000 0.626250 41.000000 1.000000
max 17.000000 199.000000 122.000000 99.000000 846.000000 67.100000 2.420000 81.000000 1.000000

Hemos observado que todas las columnas contienen valores numéricos y que estos valores varían ampliamente entre las diferentes columnas. Para garantizar un rendimiento óptimo de nuestro modelo, es necesario normalizar los datos.

In [4]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 768 entries, 0 to 767
Data columns (total 9 columns):
 #   Column                    Non-Null Count  Dtype  
---  ------                    --------------  -----  
 0   Pregnancies               768 non-null    int64  
 1   Glucose                   768 non-null    int64  
 2   BloodPressure             768 non-null    int64  
 3   SkinThickness             768 non-null    int64  
 4   Insulin                   768 non-null    int64  
 5   BMI                       768 non-null    float64
 6   DiabetesPedigreeFunction  768 non-null    float64
 7   Age                       768 non-null    int64  
 8   Outcome                   768 non-null    int64  
dtypes: float64(2), int64(7)
memory usage: 54.1 KB
In [5]:
df.isin([0]).sum()
Out[5]:
Pregnancies                 111
Glucose                       5
BloodPressure                35
SkinThickness               227
Insulin                     374
BMI                          11
DiabetesPedigreeFunction      0
Age                           0
Outcome                     500
dtype: int64

No se han encontrado columnas con datos faltantes, sin embargo, hemos notado la presencia de numerosos valores de cero en las columnas correspondientes a "insulina" y "grosor de la piel". Dado que el valor cero no es válido para representar la información que intentan describir, podemos interpretar que las mediciones en estos casos no fueron realizadas o se cargaron incorrectamente.

Además, las columnas "glucosa", "IMC" (Índice de Masa Corporal) y "presión sanguínea" también contienen valores inválidos, aunque en menor cantidad.

In [6]:
corr_matrix = np.corrcoef(df.values.T)
fig = go.Figure(data=go.Heatmap(
        z=corr_matrix,
        x=df.columns,
        y=df.columns,
        colorscale='dense'))
fig.update_layout(
    title = 'mapa de calor',
    title_x = 0.5,
    width = 1300,
    height = 1000
)
fig.show()

Hemos observado que tanto la columna de "insulina" como la de "grosor de la piel" presentan una correlación muy baja con la presencia de diabetes. Además, debido a la gran cantidad de valores inválidos en estas columnas, hemos decidido no utilizarlas en nuestra predicción.

En cambio, para las columnas de "glucosa", "IMC" (BMI) y "presión sanguínea", hemos notado una correlación más significativa con la presencia de diabetes. Para poder aprovechar esta información en nuestra predicción, vamos a rellenar los espacios vacíos con el promedio de cada columna. Esto nos permitirá utilizar de manera más completa y precisa los datos disponibles.

In [7]:
df.Glucose.replace(0,int(df.Glucose.mean()),inplace=True)
df.Insulin.replace(0,df.Insulin.mean(),inplace=True)
df.BMI.replace(0,df.BMI.mean(),inplace=True)
In [8]:
drop_columns = ["Outcome","Insulin","SkinThickness"]
df.isin([0]).sum()
Out[8]:
Pregnancies                 111
Glucose                       0
BloodPressure                35
SkinThickness               227
Insulin                       0
BMI                           0
DiabetesPedigreeFunction      0
Age                           0
Outcome                     500
dtype: int64

Ahora que hemos realizado la limpieza del dataset, es el momento de analizar la distribución de los datos. Este paso nos permitirá obtener una visión más detallada de cómo se distribuyen las variables en nuestro conjunto de datos.

In [9]:
proporcion = df.Outcome.value_counts()
proporcion.index = ['No tiene Diabetes','Tiene Diabetes']
fig = go.Figure(go.Pie(
    labels = proporcion.index,
    values = proporcion.values,
    pull=[0.05, 0],
    marker=dict(colors=['#9560d6','#4490bd'])
))

fig.update_layout(
    title = 'Distribución de casos positivos y negativos',
    title_x= 0.46,

)

fig.show()

Notamos que hay una mayor cantidad de observaciones de personas sin diabetes en comparación con aquellas que sí la tienen. Esto implica que será más fácil predecir el resultado negativo (ausencia de diabetes) que el positivo (presencia de diabetes) en nuestro modelo.

In [10]:
Cant_por_embarazo = df.groupby(['Outcome','Pregnancies']).size().reset_index(name='cantidad')
Cant_por_embarazo_pos = Cant_por_embarazo[Cant_por_embarazo['Outcome'] == 1 ]
Cant_por_embarazo_neg = Cant_por_embarazo[Cant_por_embarazo['Outcome'] == 0 ]

fig = go.Figure()

fig.add_trace(
    go.Bar(
        y=Cant_por_embarazo_neg['cantidad'],
        x=Cant_por_embarazo_neg['Pregnancies'],
        orientation='v',
        name='Caso Negativo',
        marker=dict(color='#9560d6')
    )
)

fig.add_trace(
    go.Bar(
        y=Cant_por_embarazo_pos['cantidad'],
        x=Cant_por_embarazo_pos['Pregnancies'],
        orientation='v',
        name='Caso Positivo',
        marker=dict(color='#4490bd')
    )
)


fig.update_layout(
    title = 'Distribución de casos según cantidad de embarazos',
    title_x=0.5,
    barmode='group',
    xaxis_title='Cantidad de embarazos',
)

fig.show()

Al examinar el número de embarazos, se observa que la proporción de casos positivos de diabetes es baja en aquellos con un bajo número de embarazos. Sin embargo, a partir de tres embarazos, la distribución comienza a ser más equitativa, y en la mayoría de los casos con más de siete hijos, la proporción de casos positivos es mayor.

In [11]:
fig = go.Figure()
for label in df['Outcome'].unique():
    if label == 0:
        name = 'Caso Negativo'
        color = '#9560d6'
    else:
        name = 'Caso Positivo'
        color = '#4490bd'


    fig.add_trace(go.Histogram(x=df[df['Outcome'] == label]['Glucose'],
                                xbins=dict(start=30, end=200, size=60),
                                name=name , marker=dict(color=color)))

fig.update_layout(title='Distribución de personas con diabetes según el nivel de glucosa',
                  xaxis_title='Glucosa',
                  yaxis_title='Cantidad',
                  xaxis=dict(ticktext=['Nivel de Glucosa Bajo:30-90 mg/d', 'Nivel de Glucosa medio:90-150mg/dL', 'Nivel de Glucosa alto:150-210mg/dL'],
                             tickvals=[60, 120, 180])
                  )

fig.show()

Podemos observar una fuerte relación entre el nivel de glucosa y la proporción de casos positivos de diabetes. A niveles bajos de glucosa, entre 30-90, los casos positivos representan apenas un 5% de las participantes. Sin embargo, a niveles más altos de glucosa, entre 150-210, la proporción de casos positivos aumenta significativamente, llegando a representar un 74% de los casos. Esto indica que el nivel de glucosa es un factor importante en la predicción de la presencia de diabetes.

In [12]:
fig = go.Figure()
for label in df['Outcome'].unique():
    if label == 0:
        name = 'Caso Negativo'
        color = '#9560d6'
    else:
        name = 'Caso Positivo'
        color = '#4490bd'


    fig.add_trace(go.Histogram(x=df[df['Outcome'] == label]['BloodPressure'],
                                xbins=dict(start=20, end=150, size=20),
                                name=name , marker=dict(color=color)))

fig.update_layout(title='Distribución de personas con diabetes según la presion sanguinea',
                  xaxis_title='Presion Sanguinea',
                  yaxis_title='Cantidad')
fig.show()

Cuando la presión sanguínea se encuentra en niveles normales, es decir, por debajo de 80, se observa una proporción relativamente baja de casos positivos de diabetes, aproximadamente un 30%. Sin embargo, es importante tener en cuenta que esta proporción también está influenciada por la distribución de los datos. A medida que la presión sanguínea aumenta, la proporción de casos positivos tiende a incrementarse, llegando a alcanzar el 50% en aquellas participantes con los niveles más elevados de presión sanguínea. Esta tendencia sugiere una posible asociación entre la presión sanguínea elevada y la presencia de diabetes en nuestra muestra de datos.

In [13]:
fig = go.Figure()
for label in df['Outcome'].unique():
    if label == 0:
        name = 'Caso Negativo'
        color = '#9560d6'
    else:
        name = 'Caso Positivo'
        color = '#4490bd'


    fig.add_trace(go.Histogram(x=df[df['Outcome'] == label]['Age'],
                                xbins=dict(start=20, end=90, size=10),
                                name=name , marker=dict(color=color)))

fig.update_layout(title='Distribución de personas con diabetes según la Edad',
                  xaxis_title='Edad',
                  yaxis_title='Cantidad')
fig.show()

Se observa que en el rango de edad comprendido entre los 20 y 30 años, la proporción de casos positivos de diabetes es relativamente baja, representando apenas un 20% de los casos. Sin embargo, a medida que la edad aumenta, también lo hace el porcentaje de casos positivos. En el grupo de edad de 50 a 60 años, la proporción de casos positivos alcanza un 60%. Este incremento sugiere una asociación entre la edad y la probabilidad de tener diabetes, ya que el riesgo de desarrollarla tiende a aumentar con el envejecimiento.

In [14]:
fig = go.Figure()
for label in df['Outcome'].unique():
    if label == 0:
        name = 'Caso Negativo'
        color = '#9560d6'
    else:
        name = 'Caso Positivo'
        color = '#4490bd'


    fig.add_trace(go.Histogram(x=df[df['Outcome'] == label]['DiabetesPedigreeFunction'],
                                xbins=dict(start=0, end=1, size=0.1),
                                name=name , marker=dict(color=color)))

fig.update_layout(title='Distribución de personas con diabetes según la Función Pedigrí de la Diabetes,',
                  xaxis_title='Función Pedigrí de la Diabetes',
                  yaxis_title='Cantidad')
fig.show()

La función pedigrí de la diabetes nos permite estimar el riesgo de desarrollar diabetes en función del historial familiar, vemos que los datos que nos brinda en general coinciden con la información tomada, pero no son definitorias, ya que tenemos un 50% de casos con diabetes incluso cuando la función nos otorga los valores más altos.

Conclusion¶

En resumen, al examinar todas las variables, se observa que todas ellas están relacionadas de alguna manera con la diabetes. Algunas variables, como los niveles de glucosa en sangre, son especialmente precisas para determinar la presencia de diabetes. Sin embargo, todas las variables aportan información útil en conjunto. Aunque ninguna variable por sí sola ofrece una precisión total, utilizaremos todas estas variables en nuestra predicción para aprovechar al máximo la información disponible y mejorar la precisión de nuestro modelo.

Creando los modelos predictivos¶

Dividiendo el dataset¶

In [27]:
data = df.drop(columns=drop_columns)
scaler=StandardScaler()
scaler.fit(data)
x=scaler.fit_transform(data)
y = df['Outcome']
x_train,x_test,y_train,y_test=train_test_split(x,y,test_size=0.15)

Para evitar el riesgo de sobreajuste, divido mi conjunto de datos en un 85% para entrenamiento y un 15% para evaluación. Esta división me permite evaluar la capacidad de los modelos para generalizar en datos desconocidos durante la prueba, en lugar de simplemente memorizar los datos de entrenamiento.

Además, como mencioné anteriormente, es necesario estandarizar los datos para mejorar el rendimiento del predictor. Por lo tanto, realicé la estandarización de los datos. Al estandarizar los datos, se logra una mejor performance del predictor al eliminar las diferencias de escala entre las variables y facilitar el entrenamiento del modelo. Esto asegura que las diferencias en las unidades o rangos de los datos no afecten negativamente la precisión del modelo.

Comparando los modelos¶

In [28]:
# logistic regresion
lm = LogisticRegression()
lm.fit(x_train,y_train)
lm_y_predictions=lm.predict(x_test)
lm_result = classification_report(y_test, lm_y_predictions, output_dict=True)


# Random Forest Classifier
rfc = RandomForestClassifier()
rfc.fit(x_train, y_train)
rfc_y_predictions = rfc.predict(x_test)
rfc_result = classification_report(y_test, rfc_y_predictions, output_dict=True)

# Support Vector Machines
svm = SVC(probability=True)
svm.fit(x_train, y_train)
svm_y_predictions=svm.predict(x_test)
svm_result = classification_report(y_test, svm_y_predictions, output_dict=True)


# KNeighborsClassifier

knn = KNeighborsClassifier()
knn.fit(x_train, y_train)
knn_y_predictions = knn.predict(x_test)
knn_result = classification_report(y_test, knn_y_predictions, output_dict=True)

result = pd.DataFrame(columns=['type','accuracy','precision_neg','precision_pos'])
new_rows = []

new_row_lm = {'type': 'logistic regresion', 'accuracy': lm_result['accuracy'], 'precision_neg': lm_result['0']['precision'], 'precision_pos': lm_result['1']['precision']}
new_rows.append(new_row_lm)

new_row_rfc = {'type': 'Random Forest Classifier', 'accuracy': rfc_result['accuracy'], 'precision_neg': rfc_result['0']['precision'], 'precision_pos': rfc_result['1']['precision']}
new_rows.append(new_row_rfc)

new_row_svm = {'type': 'Support Vector Machines', 'accuracy': svm_result['accuracy'], 'precision_neg': svm_result['0']['precision'], 'precision_pos': svm_result['1']['precision']}
new_rows.append(new_row_svm)

new_row_knn = {'type': 'KNeighborsClassifier', 'accuracy': knn_result['accuracy'], 'precision_neg': knn_result['0']['precision'], 'precision_pos': knn_result['1']['precision']}
new_rows.append(new_row_knn)

result = result.append(new_rows, ignore_index=True)

result
Out[28]:
type accuracy precision_neg precision_pos
0 logistic regresion 0.758621 0.788235 0.677419
1 Random Forest Classifier 0.758621 0.802469 0.657143
2 Support Vector Machines 0.775862 0.786517 0.740741
3 KNeighborsClassifier 0.741379 0.790123 0.628571

En general, se pudo observar que todos los modelos utilizados presentaron tasas de éxito similares en la predicción de los resultados, con una precisión aproximada del 78%.

Dado el desempeño ligeramente superior del modelo de Support Vector Machines, se tomará la decisión de guardarlo para su uso posterior. Al guardar el modelo, se evita la necesidad de volver a entrenarlo cada vez que se desee utilizar, lo que ahorra tiempo y recursos. Esta práctica es especialmente beneficiosa cuando se trabaja con grandes conjuntos de datos o se requiere un proceso de predicción rápido y eficiente.

In [17]:
import joblib
from sklearn.calibration import CalibratedClassifierCV

calibrated_svc = CalibratedClassifierCV(svm, cv='prefit')
calibrated_svc.fit(x_train, y_train)


# Save the model for future use
joblib.dump(svm, 'svm_model.pkl')
joblib.dump(calibrated_svc, 'calibrated_svc.pkl')
Out[17]:
['calibrated_svc.pkl']

Conclusión¶

Esto indica que existen varias condiciones médicas que pueden servir como indicadores para diagnosticar la posible presencia de diabetes, lo que a su vez permite tomar medidas preventivas para reducir las posibilidades de desarrollar esta enfermedad. Estos hallazgos resaltan la importancia de una detección temprana y el seguimiento de las condiciones médicas relacionadas con la diabetes para una intervención oportuna y la adopción de medidas preventivas adecuadas.

Aunque la tasa de éxito no es baja, dada el tamaño de nuestro conjunto de datos, ésta podría mejorarse si se dispusiera de un mayor número de pacientes en los cuales basar los modelos. Es importante tener en cuenta que no se puede garantizar el éxito del modelo para personas que no cumplan con las características de las pacientes del conjunto de datos utilizado.